package moodish.dummy;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import moodish.comm.ClientComm;
import moodish.comm.ClientSideMessage;
import moodish.comm.ServerSideMessage;
import moodish.comm.ServerSideMessage.Type;
import moodish.dummy.messages.ServerSideMessageClass;

public class ClientCommDummy implements ClientComm {

	/** ligacao tcp com o servidor */
	private ClientCommConnectionHandler connection;
	
	/** o nickname do cliente */
	private String nickName;
	
	/** A lista de mensagens recebidas pelo servidor */
	private ConcurrentLinkedQueue<ClientSideMessage> message_queue;
	
	/** Cadeado para cordenar este objecto */
	private final Lock lock = new ReentrantLock();
	
	/** Condicao que vai garantir que a thread mantem o cadeado ate que ter confirmado, 
	 *  atravez de metodos nao sincronizados, que pode o nao retirara a mensagem, 
	 *  garantindo o maximo de concurrencia, se que haja problemas de sincronismo.
	 * */
	private final Condition geting_data_condition = lock.newCondition();
	
	public ClientCommDummy() {
		this.message_queue = new ConcurrentLinkedQueue<ClientSideMessage>();
	}
	
	@Override
	public void connect(String serverAddress, String nickname) {
		try {
			Socket socket = new Socket(InetAddress.getByName(serverAddress), ServerCommDummy.PORT);
			this.nickName = nickname;
			System.out.println("[Cliente] Ligacao establecida.");
			
			new Thread(ClientCommDummy.this.connection = new ClientCommConnectionHandler(socket)).start();
			
		} catch (IOException e) {}

	}

	@Override
	public boolean isConnected() {
		return !connection.isSocketDown();
	}

	@Override
	public void disconnect() {
		this.connection.disconnect();
	}

	@Override
	public boolean hasNextMessage() {
		if(this.message_queue.size() == 0){
			return false;
		}else{
			return true;
		}
	}

	@Override
	public ClientSideMessage getNextMessage() {
		if(this.connection.isSocketDown()){
			return null;
		}else{
			this.lock.lock();
			try{
				try {
					while(!this.hasNextMessage()){
						System.out.println("[Cliente] Em espera de mensagens do servidor...");
						this.geting_data_condition.await();
					}
				} catch (InterruptedException e) {}

				return this.message_queue.poll();
			}finally{
				this.lock.unlock();
			}
		}
	}

	@Override
	public void sendMoodishMessage(String moodishMessage) {
		this.connection.sendMessage(
				new ServerSideMessageClass(ClientCommDummy.this.nickName, moodishMessage, Type.MOODISH_MESSAGE));
	}

	@Override
	public void friendship(String nickname) {
		this.connection.sendMessage(
				new ServerSideMessageClass(ClientCommDummy.this.nickName, nickname, Type.FRIENDSHIP));
	}

	@Override
	public void unfriendship(String nickname) {		
		
		this.connection.sendMessage(
				new ServerSideMessageClass(ClientCommDummy.this.nickName, nickname, Type.UNFREINDSHIP));

	}
	
	
	/**
	 * Classe que representa a ligacao tcp com o cliente. 
	 * Esta classe e uma thread que contem o socket respectivo do cliente.
	 * 
	 * */
	private class ClientCommConnectionHandler implements Runnable{

		/** Ligacao com o servidor */
		private Socket socket;
		
		/** Canal de saida de mensagens */
		private ObjectOutputStream out_data;
		
		/** Canal de entraga de mensagens*/
		private ObjectInputStream in_data;
		
		public ClientCommConnectionHandler(Socket socket) {
			this.socket = socket;
			try {
				this.doObjectConnections();
			} catch (IOException e) {}
		}
		
		/**
		 * Metodo que establece os canais de comunicacao tcp
		 * */
		private void doObjectConnections() throws IOException {
			this.out_data =  new ObjectOutputStream(this.socket.getOutputStream());
			this.in_data = new ObjectInputStream(this.socket.getInputStream());
		}
		
		/** 
		 * Desliga o cliente mas antes envia uma mensagema alerta o servidor que vai desligar 
		 * 
		 * */
		private void disconnect(){

			this.sendMessage(
					new ServerSideMessageClass(ClientCommDummy.this.nickName, null, Type.CLIENT_DISCONNECTED));
			
			try {
				this.socket.close();
			} catch (IOException e) {}
		}
		
		/** 
		 * Indica se a ligacao esta desligada 
		 * @return {@link Boolean} true, se estiver desligado, false caso contario
		 * */
		private boolean isSocketDown(){
			return this.socket.isClosed();
		}
		 		
		/** 
		 * Guarda a mensagem que recebeu na lista de mensagens recebidas
		 * @param message mensagem recebida
		 * */
		private void saveMessageRecived(ClientSideMessage message){
			ClientCommDummy.this.message_queue.add(message);
			
			// notefica caso haja threads em espera por mensagem
			ClientCommDummy.this.lock.lock();
			ClientCommDummy.this.geting_data_condition.signalAll();
			ClientCommDummy.this.lock.unlock();
			
		}
		
		/** 
		 * Envia mensagem para o servidor.
		 * 
		 * @param message mensagem a enviar
		 * */
		private void sendMessage(ServerSideMessage message){
			try {
				this.out_data.writeObject(message);
				System.out.println("[Cliente] Mensagem enviada.");
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		@Override
		public void run() {
			
			// Envio da mensagem inicial para guardar o nickname no servidor
			this.sendMessage(new ServerSideMessageClass(ClientCommDummy.this.nickName, null, Type.CLIENT_CONNECTED));
			ClientSideMessage message;
			while(!this.socket.isClosed()){
				try {
					try {
						message = (ClientSideMessage) this.in_data.readObject();
						this.saveMessageRecived(message);
						System.out.println("[Cliente] Mensagem recebida.");
					} catch (ClassNotFoundException e) {}	
				} catch (IOException e) {}
			}
	
		}
		
	}

}
